package com.wudgaby.study.jwtauth.controller;

import com.google.common.collect.ImmutableMap;
import com.wudgaby.study.jwtauth.config.TokenProperties;
import com.wudgaby.study.jwtauth.constant.RedisConstant;
import com.wudgaby.study.jwtauth.constant.SecurityConsts;
import com.wudgaby.study.jwtauth.constant.TokenConstant;
import com.wudgaby.study.jwtauth.form.LoginForm;
import com.wudgaby.study.jwtauth.support.JWTHelper;
import com.wudgaby.study.jwtauth.support.RedisSupport;
import io.jsonwebtoken.Claims;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Slf4j
@RestController
@Api(tags = "auth")
public class AuthController {
    @Autowired
    private RedisSupport redisSupport;

    @Autowired
    private TokenProperties tokenProperties;

    @Autowired
    private JWTHelper jwtHelper;

    /**
     * 生成accessToken 和 refreshToken
     * 一个用于访问 一个用于刷新访问令牌
     * refreshToken保存到redis中
     * @param loginForm
     * @return
     */
    @ApiOperation(value = "登录")
    @PostMapping("/login")
    public ResponseEntity<Object> login(LoginForm loginForm) {
        if(loginForm.getUserName().equals("admin") && loginForm.getPassword().equals("123456")){
            String accessToken = jwtHelper.createJWT(ImmutableMap.<String, Object>builder().put(SecurityConsts.USER_ID_KEY, 1).put(SecurityConsts.USER_NAME_KEY, loginForm.getUserName()).build(), tokenProperties.getAccessTokenExpire().toMillis());
            //刷新令牌放入缓存.
            String refreshToken = DigestUtils.md5Hex(UUID.randomUUID().toString());
            redisSupport.set(RedisConstant.REFRESH_TOKEN_PREFIX + accessToken, refreshToken, tokenProperties.getRefreshTokenExpire().getSeconds());
            return ResponseEntity.ok(ImmutableMap.<String, String>builder().put("accessToken", accessToken).put("refreshToken", refreshToken).build());
        }
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("账号密码错误!");
    }

    /**
     * 登出时,把当前accessToken加入到redis黑名单中. 在拦截器中检查accessToken在黑名单就进行拦截.
     * 并把refreshToken删掉.
     * @param accessToken
     * @return
     */
    @ApiOperation(value = "登出")
    @DeleteMapping("/logout")
    public ResponseEntity<String> logout(@RequestHeader(TokenConstant.ACCESS_TOKEN) String accessToken) {
        if(jwtHelper.verifyJWT(accessToken)){
            Claims claims = jwtHelper.parseJWT(accessToken);
            long diff = claims.getExpiration().getTime() - System.currentTimeMillis();
            log.info("距离过期时间差: {}", diff);
            long expSec = Math.max(1, TimeUnit.MILLISECONDS.toSeconds(diff));
            //注销的访问令牌放入缓存,并加入令牌过期时间.
            //拦截器从缓存发现注销的令牌的话,不予通过
            redisSupport.set(RedisConstant.ACCESS_TOKEN_BLACK_PREFIX + accessToken, accessToken, expSec);
        }
        //删除刷新令牌
        redisSupport.del(RedisConstant.REFRESH_TOKEN_PREFIX + accessToken);
        return ResponseEntity.ok("注销成功!");
    }

    /**
     * 通过前端传递的accessToken和refreshToken进行生成新的accessToken.
     * 在redis检查refreshToken是否在有效期内.
     * 生成新accessToken后,删除旧的accessToken和对应的refreshToken
     *
     * 并发刷新问题.用jmeter开了10个线程测试.只有一个请求能生成新的accessToken 其他都是401.
     * 即使有多个请求生成新accessToken.问题也不大.
     * @param accessToken
     * @param refreshToken
     * @return
     */
    @ApiOperation(value = "刷新")
    @PutMapping("/refreshToken")
    public ResponseEntity<Object> refreshToken(@RequestHeader(TokenConstant.ACCESS_TOKEN) String accessToken, @RequestHeader(TokenConstant.REFRESH_TOKEN) String refreshToken) {
        Claims claims = jwtHelper.parseJWT(accessToken);
        if(claims == null){
            log.warn("无效的访问令牌. {}", accessToken);
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("无效的访问令牌");
        }

        Object refreshTokenObj = redisSupport.get(RedisConstant.REFRESH_TOKEN_PREFIX + accessToken);
        if(refreshTokenObj == null){
            log.warn("刷新令牌 [{}] 已经过期.", refreshToken);
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("刷新令牌已经过期.");
        }

        long refreshTokenExpire = redisSupport.getExpire(RedisConstant.REFRESH_TOKEN_PREFIX + accessToken);
        if(!Objects.equals(refreshTokenObj.toString(), refreshToken)){
            log.warn("刷新令牌与访问令牌不符.{} != {}", refreshTokenObj.toString(), refreshToken);
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("刷新令牌与访问令牌不符.");
        }

        //防止并发产生多个新token
        if(redisSupport.hasKey(RedisConstant.ACCESS_TOKEN_BLACK_PREFIX + accessToken)){
            log.warn("{} 已经在黑名单中.", accessToken);
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(accessToken + "已经在黑名单中.");
        }

        //旧access放入到黑名单.防止并发刷新产生问题.
        long diff = claims.getExpiration().getTime() - System.currentTimeMillis();
        log.info("距离过期时间差: {}", diff);
        long expSec = Math.max(1, TimeUnit.MILLISECONDS.toSeconds(diff));
        redisSupport.set(RedisConstant.ACCESS_TOKEN_BLACK_PREFIX + accessToken, accessToken, expSec);

        //删除旧的刷新令牌
        redisSupport.del(RedisConstant.REFRESH_TOKEN_PREFIX + accessToken);

        //新访问令牌
        String newAccessToken = jwtHelper.createJWT(claims, tokenProperties.getAccessTokenExpire().toMillis());
        //新访问令牌对应的刷新令牌
        redisSupport.set(RedisConstant.REFRESH_TOKEN_PREFIX + newAccessToken, refreshToken, refreshTokenExpire);

        return ResponseEntity.ok(ImmutableMap.<String, String>builder().put("newAccessToken", newAccessToken).build());
    }

}
